"""
@-*- coding: utf-8 -*-
@ python：python 3.8
@ 创建人员：ylx
@ 创建时间：2025/2/22 上午10:50
@ 内容：系统的学习pytest测试框框，这里会包含pytest框架及自动化测试中该框架的相关的内容
"""

"""
A------入门
pytest 单元测试框架

一.什么是单元测试框架
单元测试就是指在软件开发中，针对软件的最小单位，如方法，函数，进行正确性的检查测试

二.主流的单元测试框架
Java  junit testng
python unittest pytest

三.单元测试框架主要做什么
1.测试发现，从多个文件中去找到测试用例
2.测试执行，按照一定的顺序和规则去执行，生成结果
3.测试判断，通过断言来判断预期结果和实际结果的差异
4.测试报告，统计测试进度，耗时，等等，需要生成一份测试报告

四.单元测试框架和自动化测试框架有什么关系
1.什么是自动化测试框架
自动化测试框架是为了去完成一个指定的系统的自动化测试，而封装的一整套完善的代码的框架
主要是封装自动化的一些基础模块，管理模块，统计模块
就是我们针对一个项目开发的一个代码框架，封装了很多基础模块，报告模块等，用来执行这个
项目的相关业务，并判断，并出具本次的测试报告

2.主要作用
1.提高测试效率，降低维护成本
2.减少人工干预，提高测试的准确性，增加代码的重用性
3.核心思想是想让不懂代码的人也能通过这个框架去实现自动化测试***

3.pytest单元测试框架和自动化测试框架的关系
单元测试框架只是自动化测试框架中的组成部分之一
再比如pom设计模式，也是自动化测试框架的组成部分之一
再如数据驱动、关键字驱动、全局配置文件封装、日志监控
selenium,requests二次封装，断言，报告，邮件等等
以上这些组成了你的自动化测试框架
相当于你的自动化测试框架是需要很多模块一起来合作完成，驱动业务的





B------
一.pytest简介
1.首先是一个非常成熟的python单元测试框架，比unittest更灵活，容易学习
2.pytest可以和selenium，requests，appium结合实现web自动化 接口自动化，app自动化
3.pytest中可以实现测试用例的跳过以及reruns，既让失败用例可以重试，并指定失败重试的次数
4.pytest + allure 生成测试报告，以及对报告进行拓展配置，比pytest-html报告更加美观和有可读性
5.pytest + Jenkins 持续集成，可以自动执行，自动发送测试报告至邮箱
6.有很多非常强大的插件，并且这些插件能够实现很多实用的操作
    pytest-html 生成html 格式的自动化测试报告
    pytest-xdist 用于测试用例分布式执行，多cpu分发来执行任务
    比如说你一个线程执行6个任务，1个任务睡眠3秒，单线程需要18秒至少，如果开启多线程，比如说
    2个线程，那你的时间肯定会缩短很多
    
    pytest-ordering 改变测试用例的执行顺序
    pytest-rerunfailures 用例失败后重跑
    allure-pytest 生成美观的测试报告
  
二.项目环境配置简介或依赖配置技巧
对于一个自动化测试框架，我们项目的起始的依赖会有很多
每个人电脑上的依赖或者插件可能有，可能没有，可能是低版本，可能是高版本
执行项目时可能会出现一些问题，我们采用如下方案解决：
通过在项目的根目录下新建requirements.txt文件说明项目依赖的插件
包括你还可以指定版本 == 版本号
借助相关命令，批量的去下载这些插件
将项目需要的插件的名称都放在requirements.txt文档中
pycharm终端进入此项目，请注意，一定要，必须要进入这个项目才能执行下面的命令
再次注意一定要用创建虚拟环境的Python项目，这样你使用下面的命令安装相关依赖才能做好隔离
pip install -r requirements.txt 
安装完成后，在当前这个项目中，输入pip list或者插件的名称
如pytest --version 就可以看到你按安装的这些插件是否成功，以及版本
注意：这里是安装在虚拟环境中，并不是全局配置，仅是局部的，针对我们当前的这个项目
如果你要装全局的，先cmd后 然后使用pip install，完成后使用pip list可以看你全局已经有的插件
上述涉及python环境隔离的相关知识，如果你要每个项目区分，那就可以学习python环境管理的知识，看看虚拟环境是怎么配置并使用的


三.pytest默认的测试用例规则（命名规则）及基础应用
3.1-默认命名规则
我们也可以通过pytest.ini全局配置相关规则
1.模块名必须以test_开头或者_test结尾
2.测试类必须以Test开头，并且类中不能有init方法
3.测试方法必须以test开头 详情见learn_pytest项目

3.2-pytest测试用例运行方式
1.主函数模式
a.运行所有或者运行我们当前这个模块，通过pytest.main()，括号内可以传参，参数是一个列表类型的，比如
pytest.main(["-s"])，这样可以输出用例中的打印信息或者调试信息
我们一般比如说，在你当前的这个模块内运行借助
if __name__ == "__main__":
    pytest.main()来作为入口函数，触发当前模块整体执行

参数意义：
-s 输出用例中的打印信息或者调试信息
-v 显示更加详细的信息，可以组合使用如-vs
-n 分布式运行或者多线程执行测试用例,这里我们如果是比如2个模块，启用2个线程，那分别会执行者2个模块，如果是1个模块下类中的4个方法
那你启用2个线程，2个线程会各自执行其中的几条case
举例-pytest -vs ./testcase -n 2启动2个线程执行，如果是代码的话就是列表中添加一个"-n=2"
这里指定了我们要去当前目录下的testcase目录及其子目录下寻找符合条件的测试用例并执行
--reruns=2 失败后再重试2次，格式为pytest.main(["-s",'--rerun=2']),注意安装相关的pytest插件，才可以完成这个

-x 只要一个报错，整体测试停止
--maxfail=2 最大失败次数，出现2个失败就停止

-k 执行指定的测试用例，模糊匹配
pytest -vs ./testcase -k "ao"  执行方法名中有ao的测试用例，根据测试用例部分字符串指定测试用例

--html=./report/report.html这个也可以加到主函数列表中执行，生成html报告

**注意下面的例子讲解时，是在根目录下有个整体的触发为前提
来写相关的pytest执行的，所以pytest执行时，取决你这个触发所在的层级

b.执行当前目录下，指定模块的
参数后面可以再加个指定文件名称即可
pytest.main(["-s"， "test_02_login.py"])

c.指定目录执行
比如说你的case分为UI和接口的，分文件夹管理
你现在只想执行UI目录下的，那你就要这样写，前提是你的all.py需要和这2个文件夹在一个层级
也就是all.py和这2个文件夹均属于第一级目录
pytest.main(["-s"， ./UI_testcase"]),就可以执行固定目录下的测试用例

d.通过nodeid指定用例运行,这个用例可能是单独的case，也可能是某个类下的case，都可以通过
当前这种方式来指定到用例层面来精准运行某个用例
nodeid由模块名-类-方法-函数组成，中间用::隔开
如果我仅想执行 interface_case 下的test_login 内的一个方法
我可以在整体的all.py中这样写
执行，不在类中的函数
pytest.main(["-vs", "./interface_testcase/test_interface.py::test_02_interface"]),
执行类中的某个方法
pytest.main(["-vs", "./interface_testcase/test_interface.py::TestLogin::test_02_interface"]),

2.命令行模式
a.运行所有 进入项目文件夹后输入pytest，它会自动查找符合执行条件的case，范围是当前目录下及相关子目录
添加相关参数执行 pytest -vs

b.运行部分，你不能进入项目后就执行 pytest -vs test_02_login.py
会报无法找到，你必须要去这个文件的文件夹内执行才可以,因为你给的是一个相对路径，你给绝对路径的话也可以执行

c.指定目录，执行目录下所有能被找到，符合条件的测试模块
pytest -vs ./interface_testcase 

d.指定模块，通过nodeid的方式运行个别测试用例，如函数或类中的方法
具体可以参考主函数模式
pytest -vs ./interface_testcase/test_interface.py::TestLogin::test_02_interface


3.通过读取pytest.ini 配置文件来运行
这也是真实的在企业的自动化测试中使用的，
但是，前2种我们也要会，也要知道
pytest.ini这个文件，它是pytest单元测试框架的核心配置文件
位置：一般是放在项目的根目录上，名字不能修改，必须是pytest.ini

编码：格式必须是ANSI，可以使用nod_pad++修改编码格式
报错的话就文件拿出来，用这个软件打开，选择编码，选择ANSI格式，然后重新放回去，一般来说
我们在pycharm中新建pytest.ini文件实际上就是符合需要的，可以直接使用

作用：它可以改变pytest默认的行为,比如说case命名规则，执行哪个目录下，使用什么参数，包括你可以标记用例，如冒烟等

运行的规则：不管你使用主函数运行或者命令行运行，都会去读取这个配置文件
在这个配置文件中我可以指定执行测试case路径 包括默认的模块/类/方法名的命名规则，指定执行时添加的参数等
所以我们后续的执行中只要pytest就行，因为执行时，它会首先访问默认的配置文件读取相关参数，具体的模版如下
[pytest]
addopts = -vs ; 命令行参数，多个参数用空格分开，这个就是笔记里pytest执行时支持的参数--这个注释一般要干掉；命令行参数，用空格分开，若有其他参数进来的话
          --html ./report/report.html ; 这是生成pytest-html报告的参数，提前创建一个report文件夹

testpaths = ./testcase ; 测试用例文件夹，一个点表示当前目录，2个点表示返回上一级---测试用例路径
python_files = test_*.py ; 配置测试搜索的文件名称，表示你的模块名以什么开头---模块名规则
python_classes = Test; 配置测试搜素的测试类名--类名规则
python_functions = test ;配置测试搜索的测试函数名--方法名规则

问题：
我在执行pytest.main()时发现了如下现象
我的pytest.ini在根目录下，我根下还挂了2个文件夹分别是，testcase和interface_testcase
我进入后者目录下某个测试文件中，里边配置了if __name__ == "__main__": pytest.main(),触发执行
因为我跟目录下配置的pytest.ini文件中配置testpaths=./testcase，我的预期是应该要执行pytest.ini配置的路径下
的相关测试文件，可实际看起来是读取了ini配置文件中其它参数配置，唯独路径没有，执行的就是我当前这个模块的测试用例
我发现，我虽然有配置文件，但是执行时仅仅是执行了当前对应的模块下的测试用例，为什么没有按照配置的路径去执行呢？
解释：
主要原因还是如果当前被调用的模块是一个符合要求的测试文件，里边存在相关的测试用例
既存在符合规则的测试文件
pytest 会优先执行这些文件，而忽略 testpaths 的限制。
而你通过根目录下的all.py去执行时，这个文件首先不遵守pytest 测试用例的命名规则，此时就会按照
pytest.ini配置的路径执行，而当你是在符合规矩的测试文件中执行时，默认就是你当前工作的目录，忽略testpaths的配置

其实想想也对，你进入了某个测试文件中执行，你肯定是想测试这个文件本身，如果还按照配置来做，那就太不灵活了
如果 pytest.main() 是在某个测试文件内部被调用，pytest 会“智能”地认为：“用户想测试这个文件”，所以只收集当前文件或当前目录
这也确实证明了，pytest会往上逐级寻找ini文件，找到后既按配置执行
如果不是从测试文件中触发 pytest.main() 那么pytest会完整的遵循pytest.ini的配置
如果是从测试文件中触发，pytest智能的决定你是想重新测试这个文件，所以会忽略ini文件中配置的testpaths

要避免这种情况-可以这么做-要确保 testpaths 配置生效
可以在根目录，通过all.py中下运行pytest.main()，或显式指定路径，最好还是在根目录下来使用
新建一个all.py作为触发，仅执行pytest.main()，然后通过根目录下的ini配置文件结合使用，这样可以确保你正确读取
pytest.ini的相关配置文件，按照你的预期去完成相关测试

当前工作目录：/interface_testcase,命令行进入后直接执行pytest，为什么也没有按照根目录下ini文件配置的路径执行对应目录下的测试用例
pytest 行为：
pytest 会从当前工作目录 /interface_testcase 向上查找 pytest.ini 文件。
找到根目录下的 pytest.ini 文件后，pytest 会加载其中的配置。
关键点：尽管 testpaths = ./testcase 配置了扫描范围，但是你在B文件夹下执行，pytest默认执行路径为 pytest .
且当前工作目录 /interface_testcase 中存在符合规则的测试文件（如 test_a.py 和 test_b.py）
pytest 会优先执行这些文件。

“如果你在子目录下运行 pytest（无参数），我默认你只想测试这个子目录，所以我会把 . 当作显式路径，忽略 testpaths。”
这不是“bug”，而是 pytest 的“用户体验优化” —— 它假设：
你在根目录跑 pytest → 想跑整个项目的测试（用 testpaths）
你在子目录跑 pytest → 想快速测试这个子模块（忽略 testpaths
结果：pytest 会执行 interface_testcase/test_a.py 和 interface_testcase/test_b.py，而不是 testcase/test_1.py 和 testcase/test_2.py。
pytest（无参数）  或  pytest . + 在根目录 → testpaths 生效
pytest .（有参数） + 子目录 → testpaths 都不生效
pytest（无参数）+ 在子目录 → pytest 认为你“有上下文意图”，不触发 testpaths

如果你在根目录下运行 pytest 或 pytest .，我默认你想要的是项目配置的默认测试路径（testpaths），而不是扫描整个项目根目录。
你在根目录下执行 pytest .，pytest 仍然走了 testpaths 而不是执行根目录下的测试文件 —— 这是因为 testpaths 的优先级高于“当前目录扫描”
但仅限于“根目录 + 无参数或 .” 的特殊情况！这是 pytest 的“智能默认行为”，不是 bug！

pytest . + 根目录 有testpaths配置，且根目录下有其他测试文件，此时执行还是走testpaths
pytest . + 子目录 或 pytest + 子目录  或 某个测试文件中执行pytest.main()  均不使用testpaths，而是当前目录下 当前文件 为测试路径


想解决这种问题，要么你强制指定执行的文件或者路径或者你就按如下的逻辑来操作就可以0

逻辑：
pytest 在启动时会：
检查是否在“项目根目录”（有 pytest.ini 的目录）
检查是否配置了 testpaths
检查命令行参数：
如果无参数 → 使用 testpaths
如果参数是 . 且在根目录 → 仍然使用 testpaths（特殊处理！）
如果参数是 . 且在子目录 → 忽略 testpaths，扫描当前目录
如果参数是具体文件或目录 → 忽略 testpaths，执行指定路径

4.pytest执行测试用例的顺序是怎么样的？
unittest 按ASCII码的大小来决定执行的顺序，大的先执行
ASCII码的顺序是小写字母 (a-z) > 大写字母 (A-Z) > 数字 (0-9) > 特殊字符
所以从小到大就是   数字 0-9  大写 A-Z 小写  a-z
pytest默认从上到下，我们可以指定测试用例的执行顺序，使用@pytest.mark.run(order = 1)
被标记的按照标记顺序来，没有标记的再被标记的执行完后，按照默认顺序来执行
这个功能依赖 pytest-ordering，所以务必记得安装相关的插件，需要依赖相关的pytest插件


5.pytest如何分组执行case？比如说我要执行所有的冒烟用例
之前我们都是指定1个类，1个方法，1个文件夹下的整体来执行
那我们能不能指定这个目录下的一部分，另外一个目录下的一部分这样去执行呢？
为什么会有这样的需求-
因为我们的冒烟用例肯定是分布在不同的模块中的
我们该如何去把这种用例挑选出来取执行，如果仅仅依赖pytest.ini配置文件中testpaths的指定
是不可能高效的完成这个目标的，因此我们使用下面的方法，对用例进行标记，运行时仅运行拥有这些标签的用例：
应用范围--冒烟测试、分模块执行、分接口，分UI执行
在全局配置文件pytest.ini中增加如下配置：
markers = 
        smoke:冒烟用例
        usermanage:用户管理模块
        productmanage:产品管理模块

然后借助@pytest.mark.smoke这种样式来完成用例标记
你需要把这个放在你具体的case之前，作为标记
关于smoke及对应的解释可以在python.ini中配置
命令行执行时增加参数
pytest -vs ./ -m 'smoke'
还支持既执行有这种标记的也执行有另外一种标记的使用or隔开
如果要同时符合有2个标记的，使用and隔开
pytest -vs ./ -m 'smoke or usermanage'
但是其实上面执行的路径已经不需要你写了
因为执行时默认回去pytest.ini配置文件查找，已经申明了testpaths
包括这个vs，我们也会在pytest.ini配置文件中声明  
我们这里其实可以只写配置文件没有的参数
简化的写法可以写成这样，比如
pytest -m 'smoke'


6.pytest跳过测试用例
a.无条件跳过，既只要有这个装饰器就跳过
借助@pytest.mark.skip(reason='原因')  有这个就跳过
b.动态的条件跳过，满足条件就跳过
需要你在当前的测试类中设置一个全局变量，也可以理解为是一个类属性，比如说：  age = 18
@pytest.mark.skipif(age >= 18,reason = '已成年')
如果你当前满足，既你的条件表达式为True时，跳过该测试用例

这里我们的第一个参数也可以是一个方法的调用
如下例子
在这个例子中，如果 is_external_service_down() 返回 True
那么 test_external_service 测试将被跳过，并显示指定的原因。
import pytest

# 定义一个条件函数
def is_external_service_down():
    # 这里可以是一个检查外部服务状态的逻辑
    return True  # 假设外部服务确实不可用

@pytest.mark.skipif(is_external_service_down(), reason="外部服务不可用")
def test_external_service():
    assert external_service_call() == expected_result


c.pytest.skip()：用于在测试函数内部根据条件动态跳过测试，如下例子：
import pytest

def check_condition():
    # 这里可以是一个检查某些条件的逻辑
    return False  # 假设条件不满足

def test_dynamic_skip():
    if not check_condition():
        pytest.skip("条件不满足，跳过测试")
    # 继续执行测试的其余部分
    assert some_function() == expected_result


@pytest.mark.skip(reason="")无条件跳过测试
@pytest.mark.skipif(age > 17, reason='')根据条件决定是否跳过测试，提供清晰的原因来帮助团队理解为什么此次测试被跳过


pytest.param在@pytest.mark.parametrize中结合使用
可以用于在参数化测试中根据参数值跳过某些测试用例
@pytest.mark.parametrize是用于参数化测试函数，让一个测试函数用多组参数运行多次
import pytest

@pytest.mark.parametrize("x, y", [
    (1, 2),
    (3, 4),
    (5, 6)
])
def test_add(x, y):
    assert x + y > 0
    
    
pytest.param可以结合在@pytest.parametrize中使用，是增强版的参数构造器，允许你
为某个参数组合添加标记，添加测试用例ID，添加跳过，失败，xfail行为

举例：
import pytest

@pytest.mark.parametrize("x, y", [
    pytest.param(1, 2),
    pytest.param(3, 4),
    pytest.param(5, 6),
])
def test_add(x, y):
    assert x + y == x + y

等价于 @pytest.mark.parametrize("x, y", [(1,2), (3,4), (5,6)])

但是pytest.param 为后续高级功能打下基础

可以自定义测试ID
举例：
@pytest.mark.parametrize("username, password", [
    pytest.param("admin", "123456", id="valid_admin_login"),
    pytest.param("guest", "guest", id="valid_guest_login"),
    pytest.param("", "123", id="empty_username"),
    pytest.param("user", "", id="empty_password"),
])
def test_login(username, password):
    print(f"测试用户: {username}, 密码: {password}")
    # 假设这里调用登录逻辑
    assert isinstance(username, str)
    
输出：
test_demo.py::test_login[valid_admin_login] PASSED
test_demo.py::test_login[valid_guest_login] PASSED
test_demo.py::test_login[empty_username] PASSED
test_demo.py::test_login[empty_password] PASSED

对比默认的 [admin-123456] 更可读、可维护、可定位！

标记某些用例为预期失败 xfail 
@pytest.mark.parametrize("input, expected", [
    pytest.param(2, 4, id="2_squared"),
    pytest.param(3, 9, id="3_squared"),
    pytest.param(4, 15, id="4_squared_wrong", marks=pytest.mark.xfail(reason="故意写错")),
])
def test_square(input, expected):
    assert input * input == expected

输出：
test_demo.py::test_square[2_squared] PASSED
test_demo.py::test_square[3_squared] PASSED
test_demo.py::test_square[4_squared_wrong] XPASS  # 如果实际通过了，会是 XPASS；如果失败，是 XFAIL
    
xfail 表示“这个用例目前会失败，但我知道，不算 bug”。 


跳过某些参数组合 skip skipif：
import sys

@pytest.mark.parametrize("os_name", [
    pytest.param("windows", marks=pytest.mark.skipif(sys.platform != "win32", reason="仅 Windows")),
    pytest.param("linux", marks=pytest.mark.skipif(sys.platform != "linux", reason="仅 Linux")),
    pytest.param("mac", marks=pytest.mark.skip(reason="暂不支持 Mac")),
])
def test_os_specific(os_name):
    print(f"运行在 {os_name} 系统")
    assert os_name in ["windows", "linux", "mac"]
    

还可以组合多个marks
既marks里边可以写多个比如：
pytest.param("a", "b", "ab", 
                 id="string_concat",
                 marks=[
                     pytest.mark.xfail(reason="当前不支持字符串"),
                     pytest.mark.slow  # 自定义 mark
                 ])

还可以配合indirect=True 实现间接参数化
举例：
@pytest.fixture
def database(request):
    db_type = request.param
    if db_type == "mysql":
        print("启动 MySQL")
        yield "MySQL"
    elif db_type == "postgres":
        print("启动 PostgreSQL")
        yield "PostgreSQL"

@pytest.mark.parametrize("database", [
    pytest.param("mysql", id="mysql_db"),
    pytest.param("postgres", id="postgres_db"),
], indirect=True)
def test_db_connection(database):
    assert database in ["MySQL", "PostgreSQL"]这段代码为什么说间接实现了参数化，这个indirect参数什么意思

回答：
一般来说，直接参数化为：默认indirect=False
@pytest.mark.parametrize("x", [1, 2, 3])
def test_foo(x):
    assert x > 0
    
    
上述代码正因为测试用例使用了database fixture，而参数名称也叫database，如果直接参数化，那么参数
应该直接传给database，但是database又是一个fixture，这就产生了矛盾
我们使用indirect=True 目的就是不要把参数直接传递给测试函数，而是把参数传递给fixture的request.param
然后让fixture去处理，将fixture处理后，yield返回的值吐给测试函数
如果不使用indirect=True 类似上面的代码执行一定会报错

我们还可以使用多个间接参数
@pytest.mark.parametrize("browser, user_role", [
    pytest.param("chrome", "admin", id="chrome_admin"),
    pytest.param("firefox", "guest", id="firefox_guest"),
], indirect=True)  # 注意：两个参数都必须是 fixture！

你需要定义2个fixture
@pytest.fixture
def browser(request):
    return launch_browser(request.param)

@pytest.fixture
def user_role(request):
    return create_user(request.param)
    
    
注意：如果只 browser 是 fixture，user_role 不是，那就不能对整个元组设 indirect=True。
此时要用：indirect=["browser"]（只对部分参数间接）
@pytest.mark.parametrize("browser, user_role", [...], indirect=["browser"])


fixture 参数化时，参数名必须是request，这是pytest的规定
request.param 是pytest自动注入的属性，你不可更改
fixture其他参数可以自定义，比如说你要依赖另一个fixture






两者相结合可以更加灵活的在参数化测试中去决定哪些执行，哪些不被执行，可以依据不同的条件来选择执行对应的参数
根据环境配置跳过：
import pytest
import sys

@pytest.mark.parametrize("version", [
    "v1",
    pytest.param("v2", marks=pytest.mark.skipif(sys.platform != "linux", reason="仅支持 Linux")),
    "v3",
])
def test_api(version):
    print(f"Testing {version}")
    assert version in ["v1", "v2", "v3"]
    
在Windows上运行
test_api[v1] PASSED
test_api[v2] SKIPPED (仅支持 Linux)
test_api[v3] PASSED


真正的动态跳过，就是在测试函数的内部根据运行时的条件，来调用pytest.skip()
import pytest

@pytest.mark.parametrize("x", [1, 2, 3, 4])
def test_dynamic_skip(x):
    if x == 3:
        pytest.skip("x=3 时动态跳过")
    assert x < 5
    
这是运行时跳过，不是参数化声明的，这才是动态跳过，只有运行时才能决定是否跳过
而上述的参数化配置时，就确定了哪些会被跳过
pytest.param 和 @pytest.mark.parametrize 可结合使用，在定义参数化测试时
为特定参数值标记跳过条件，从而实现‘按参数值跳过测试用例’的效果。


B------学习目标
1.pytest前后置处理
通过conftest.py文件实现测试用例/测试文件/整个模块/整个项目的前后置操作
问：什么是前后置处理
答：就是你想固定的在测试用例，测试类，或者测试模块执行开始前或结束后的统一操作
pytest预置处理，重复操作一般我们通过使用fixture装饰器来完成，fixture装饰器更加的灵活
“前后置处理” 指的是在测试用例执行前（前置）和执行后（后置）自动运行的代码 —— 用于准备测试环境、清理资源、初始化数据、关闭连接等

2.pytest自动化测试框架中使用params参数实现参数化
既我们一个测试用例，可能由于数据的不同而产生不同的结果，那我通过某种方式
只用1条测试用例，通过调整入参的不同就完成相关的正向和反向的case，避免代码冗余
否则你可能要根据参数不同写很多看起来重复的代码

3.pytest集成allure生成企业级的测试报告，报告改进，比如说个性化allure报告


B1.pytest框架实现前后置（网络上也有的叫固件或者夹具，但是都是一个意思）的处理，我们通常有以下几种实现方式：
1.setup/teardown方法，用于类级别或者方法级别，兼容unittest
写在测试类中，在每个测试类中的测试用例执行前和后执行相关的操作
从目前使用python3.10 pytest8.3.4使用来说，目前不能识别到前后置操作
虽然都说这个能用，但是目前看来不行

2.setup_class/teardown_class，类级别-写在测试类中
在每个测试类执行时，开始和结束执行的操作
举例：
import pytest

class TestClass:

    @classmethod
    def setup_class(cls):
        print("【前置】整个类开始前执行一次")

    @classmethod
    def teardown_class(cls):
        print("【后置】整个类结束后执行一次")

    def test_a(self):
        print("运行 test_a")
        assert True

    def test_b(self):
        print("运行 test_b")
        assert True

输出：
【前置】整个类开始前执行一次
运行 test_a
.运行 test_b
.【后置】整个类结束后执行一次


3.setup_method / teardown_method 方法级别
举例：
import pytest

class TestClass:
    
    @staticmethod
    def setup_method(method):
        print(f"【前置】准备测试方法：{method.__name__}")
    
    @staticmethod
    def teardown_method(method):
        print(f"【后置】清理测试方法：{method.__name__}")

    def test_a(self):
        print("运行 test_a")
        assert True

    def test_b(self):
        print("运行 test_b")
        assert True
        

输出：
【前置】准备测试方法：test_a
运行 test_a
.【后置】清理测试方法：test_a

【前置】准备测试方法：test_b
运行 test_b
.【后置】清理测试方法：test_b

4.fixture 推荐，最强大，最灵活，可复用，可以作用于函数/类/模块/会话级别，也能依赖其他的fixture

问：我们为什么需要这些功能呢？
答：举例：
我们在进行web自动化时，除了业务自身需要的步骤之外，执行之前、执行之后都会有一些公共的操作
比如说，用例执行之前-打开浏览器、用例执行之后-关闭浏览器
因为每个测试用例都要执行这样的操作，所以框架才诞生了这样的功能
既提供一种公共能力，避免代码冗余
a.setup/teardown  
在测试类中，每个测试步骤之前执行，和每个测试步骤之后执行

b.setup_class/teardown_class  
在测试类执行时，在类之前执行1次，类之后执行1次
既在这个类下的用例之前和之后仅执行1次，最开头 最结尾
日常的企业实践中
比如说我们在测试之前要创建日志对象，数据库连接，创建接口请求对象
这个方法相当于在所有的该类下的测试用例运行前只执行1次
同样，后面的teardown_class是所有的该类的测试用例运行完成后执行1次
比如说：来执行销毁日志对象，断开数据库连接等
注意这个是写在类中的，作用域就是当前测试类

虽然课程中提到了pytest也可以使用这个，但是实际操作中并没有执行
所以可能，最新的pytest已经不支持这种了，这并不重要！
使用更好用、灵活的方式来实现这个需求，如下操作：
pytest中使用@pytest.fixture装饰器来实现部分用例，测试工作的前置或后置工作
针对之前的setup或者teardown是在每个测试用例的前后去执行，而我们现在的需求是想在
这个测试类下的A用例前执行，B用例前不执行，使用上述提到的方法无法完成
因为不够灵活，它的颗粒度决定了
要么在类下的测试用例开始前，执行，并在类下的用例执行完后，再次执行
要么在类下的每个测试用例执行开始前，执行完成后执行，并不能更加灵活的支持我们的需求

现在我们使用如下方法
@pytest.fixture
支持如下参数：
scope：表示的是被@pytest.fixture标记的方法的作用域，支持以下参数：

function（默认作用域为函数），如果是作用于函数范围，那么你可以参考logout的例子
a.如果你要实现对单个case的前置后置操作，需要给case中，作为参数形式，传入你装饰的这个函数的名称即可
b.如果你这个case有很多，比如说你要执行全部的，可以设置 autouse=True，来让他自动执行（根据你设置的作用域）
1.如果你的夹具是写在测试用例中的，你开启这个后仅仅针对你当前的这个模块有作用
根据你的作用域，来决定他什么时候执行
比如说，作用域是函数，那么该模块下每个测试用例执行前或后都会执行相关操作，前提是要么你打开了autouse=True，或者你手动将夹具的名称传给测试用例
2.如果你的fixture统一使用conftest.py管理起来，那么你的作用域可以是你当前目录及子级模块，开启autouse=True，后不用传入，自动执行
Q/A：
你的作用范围是函数时，若你打开了autouse，虽然你按之前的做法，将函数名传给case
但是这个模块或者类下所有的测试用例都会有预期的前后置操作，就是因为你开了自动

class-类-如果你的作用范围是类级别的，相当于是在类执行前和类完全执行完成后再执行相关的前置或者后置操作
类似于setup_class/teardown_class的功能
如果你这个模块你有2个class,那你在执行这个模块的用例时，会在每个类
开始之前执行一次（若有），这个类中的case执行完后，再执行一次后置方法（若有）

module-模块-在你的模块开始和最后结束时执行1次，你的模块中可以有多个类
那就会在你所有的类初始时执行1次，

package/session-项目开始执行前执行一次，项目执行完成后，执行一次，或者这个包类的项目执行前后，注意相关概念    

autouse 自动执行-True 默认False 不自动执行

举例：fixture
import pytest

@pytest.fixture
def my_fixture():
    print("【前置】执行了！")
    yield "some_data"  # ← 测试函数运行的地方
    print("【后置】执行了！")

def test_case1(my_fixture):
    print("测试用例1运行中，收到数据：", my_fixture)
    assert my_fixture == "some_data"

def test_case2(my_fixture):
    print("测试用例2运行中")
    assert True


输出
【前置】执行了！
测试用例1运行中，收到数据： some_data
.【后置】执行了！

【前置】执行了！
测试用例2运行中
.【后置】执行了！

fixture的作用域如下
@pytest.fixture(scope="function")  # 默认，每个测试函数调用一次
@pytest.fixture(scope="class")     # 每个测试类调用一次
@pytest.fixture(scope="module")    # 每个模块（.py文件）调用一次
@pytest.fixture(scope="session")   # 整个测试会话调用一次

module级别的fixture
import pytest

@pytest.fixture(scope="module")
def db_connection():
    print("【前置】连接数据库")
    yield "DB_CONN"
    print("【后置】关闭数据库")

def test_user(db_connection):
    print("测试用户功能，使用：", db_connection)
    assert db_connection == "DB_CONN"

def test_order(db_connection):
    print("测试订单功能，使用：", db_connection)
    assert True


输出：
【前置】连接数据库
测试用户功能，使用： DB_CONN
.测试订单功能，使用： DB_CONN
.【后置】关闭数据库

发现，虽然你传了fixture，但是相关执行还是符合预期的，传的目的是为了用yield返回的对象



使用fixture实现参数化测试
1.什么是参数化fixture：
在 fixture 上加 params 参数，就可以让它“产出多个值”，每个值都会触发一次测试函数执行
import pytest

@pytest.fixture(params=[1, 2, 3])
def number(request):  # ← 必须用 request 对象获取参数值
    return request.param

def test_number(number):
    print(f"测试数字: {number}")
    assert number > 0
    

输出：
test_number[1] ✓
test_number[2] ✓
test_number[3] ✓

测试函数 test_number 被执行了 3 次，分别传入 1、2、3！   

使用@pytest.mark.parametrize
@pytest.mark.parametrize("number", [1, 2, 3])
def test_with_parametrize(number):
    assert number > 0

使用fixture
@pytest.fixture(params=[1, 2, 3])
def number(request):
    return request.param

def test_with_fixture(number):
    assert number > 0

使用fixture做参数化的优势：
1.可以做前置后置的处理
@pytest.fixture(params=["chrome", "firefox", "safari"])
def browser(request):
    print(f"【前置】启动 {request.param} 浏览器")
    driver = f"Driver_{request.param}"
    yield driver
    print(f"【后置】关闭 {request.param} 浏览器")

def test_browser(browser):
    print(f"正在使用: {browser}")
    assert "Driver" in browser
    
    
输出：
【前置】启动 chrome 浏览器
正在使用: Driver_chrome
.【后置】关闭 chrome 浏览器

【前置】启动 firefox 浏览器
正在使用: Driver_firefox
.【后置】关闭 firefox 浏览器

【前置】启动 safari 浏览器
正在使用: Driver_safari
.【后置】关闭 safari 浏览器

每个参数都能做到独立的前后置，这是使用@pytest.mark.parametrize做不到的

2.优势2：结合scope控制生命周期，比如说这次测试你想跑测试和演示环境的数据库
意味着你需要2个不同的数据库实例
@pytest.fixture(scope="module", params=["db1", "db2"])
def database(request):
    print(f"【前置】连接 {request.param}")
    conn = f"Connection_{request.param}"
    yield conn
    print(f"【后置】断开 {request.param}")

def test_db1(database):
    print(f"测试数据库: {database}")
    assert "Connection" in database

def test_db2(database):
    print(f"另一个测试: {database}")
    assert True
    
每个数据库参数会在整个模块的所有测试中共享同一个链接

优势3：可以为参数指定ID或者别名
@pytest.fixture(params=[1, 2, 3], ids=["one", "two", "three"])
def number(request):
    return request.param

def test_number(number):
    assert number > 0
    

测试报告中显示
test_number[one]
test_number[two]
test_number[three]


优势4.跳过某些参数值
import pytest

@pytest.fixture(params=[
    1,
    pytest.param(2, marks=pytest.mark.skip("暂时跳过")),
    3
])
def number(request):
    return request.param

def test_number(number):
    assert number > 0
    
    
输出：
test_number[1] ✓
test_number[2] s (暂时跳过)
test_number[3] ✓

这是 parametrize + pytest.param 也能做的，但 fixture 同样支持！

优势5.参数化fixture 可以被其它fixture所依赖
什么是fixture依赖：
在 pytest 中，fixture 不是孤立的函数，而是一个“可组合、可依赖、可复用”的生态系统
你可以让：
让 fixture A 使用 fixture B 的返回值
让 fixture A 依赖 fixture B 的前后置逻辑
pytest 会自动解析依赖关系，按正确顺序执行

原理：
当你在 fixture 函数的参数中写另一个 fixture 的名字时，pytest 会
自动查找并执行被依赖的 fixture
获取其 yield 或 return 的值
传入当前 fixture
按依赖顺序执行前后置（后置是反向的！）
后置（teardown）执行顺序：和前置相反！ —— 先执行“最后初始化”的 fixture 的后置。
基础例子：
import pytest

@pytest.fixture
def database():
    print("【前置】连接数据库")
    db_conn = "DB_CONNECTION"
    yield db_conn
    print("【后置】关闭数据库")

@pytest.fixture
def user(database):  # ← 依赖 database fixture
    print(f"【前置】使用数据库创建用户，db={database}")
    user_obj = f"User_in_{database}"
    yield user_obj
    print("【后置】清理用户")

def test_user_profile(user):  # ← 依赖 user fixture
    print(f"测试用户: {user}")
    assert "User" in user
    
    
执行流程：
pytest 发现 test_user_profile 需要 user
user 需要 database → 先执行 database
执行 database 前置 → 返回 "DB_CONNECTION"
执行 user 前置 → 使用 database 的值 → 返回 "User_in_DB_CONNECTION"
执行 test_user_profile
测试结束 → 执行 user 后置
最后执行 database 后置

输出
【前置】连接数据库
【前置】使用数据库创建用户，db=DB_CONNECTION
测试用户: User_in_DB_CONNECTION
.【后置】清理用户
【后置】关闭数据库
    
多层依赖+作用域
import pytest

@pytest.fixture(scope="session")
def config():
    print("【前置】加载全局配置")
    cfg = {"env": "test", "timeout": 10}
    yield cfg
    print("【后置】清理配置")

@pytest.fixture(scope="module")
def api_client(config):  # ← 依赖 config
    print(f"【前置】初始化 API 客户端，环境={config['env']}")
    client = f"APIClient_{config['env']}"
    yield client
    print("【后置】关闭 API 客户端")

@pytest.fixture
def logged_in_user(api_client):  # ← 依赖 api_client
    print(f"【前置】用户登录，使用客户端={api_client}")
    token = "USER_TOKEN"
    yield token
    print("【后置】用户登出")

def test_api_call(logged_in_user):
    print(f"使用 token 调用 API: {logged_in_user}")
    assert logged_in_user == "USER_TOKEN"
    
    
输出：
【前置】加载全局配置
【前置】初始化 API 客户端，环境=test
【前置】用户登录，使用客户端=APIClient_test
使用 token 调用 API: USER_TOKEN
.【后置】用户登出
【后置】关闭 API 客户端
【后置】清理配置

 注意：后置是反向执行的！最后初始化的最先清理！
    
    
fixture依赖 +  参数化
import pytest

@pytest.fixture(params=["chrome", "firefox"], ids=["Chrome", "Firefox"])
def browser_name(request):
    return request.param

@pytest.fixture
def driver(browser_name):  # ← 依赖参数化 fixture
    print(f"【前置】启动 {browser_name} 浏览器")
    drv = f"Driver_{browser_name}"
    yield drv
    print(f"【后置】关闭 {browser_name}")

def test_browser(driver):
    print(f"正在测试: {driver}")
    assert "Driver" in driver
    
输出：
【前置】启动 chrome 浏览器
正在测试: Driver_chrome
.【后置】关闭 chrome

【前置】启动 firefox 浏览器
正在测试: Driver_firefox
.【后置】关闭 firefox

上述例子表明，参数化的fixture也是可以被依赖的


依赖多个fixture
@pytest.fixture
def username():
    return "Alice"

@pytest.fixture
def password():
    return "secret123"

@pytest.fixture
def login_token(username, password):  # ← 依赖两个 fixture
    print(f"用 {username} 和 {password} 登录")
    return f"token_for_{username}"

def test_login(login_token):
    print(f"获得 token: {login_token}")
    assert "Alice" in login_token

pytest 会并行初始化 username 和 password，然后执行 login_token！

针对单个fixture依赖多个fixture的调度机制为：
pytest 使用 “拓扑排序” 来解决依赖关系：
如果 A 依赖 B → B 必须先执行
如果 A 依赖 B 和 C，且 B 和 C 无依赖 → pytest 会按参书写顺序，执行 B 和 C
重点：同级 fixture 按书写序执行
所以如果你依赖的fixture互相独立，且作用域相同，写的顺序就是执行的顺序

依赖多个无依赖关系的fixture
import pytest

@pytest.fixture
def fixture_a():
    print("【前置】fixture_a")
    yield "A"
    print("【后置】fixture_a")

@pytest.fixture
def fixture_b():
    print("【前置】fixture_b")
    yield "B"
    print("【后置】fixture_b")

@pytest.fixture
def fixture_c():
    print("【前置】fixture_c")
    yield "C"
    print("【后置】fixture_c")

@pytest.fixture
def combined(fixture_b, fixture_a, fixture_c):  # ← 依赖顺序：b, a, c
    print("【前置】combined")
    yield (fixture_a, fixture_b, fixture_c)
    print("【后置】combined")

def test_combined(combined):
    print(f"测试中收到: {combined}")
    assert len(combined) == 3
    

实际执行顺序：
【前置】fixture_b   ← b
【前置】fixture_a   ← a
【前置】fixture_c   ← c
【前置】combined
测试中收到: ('A', 'B', 'C')
.【后置】combined
【后置】fixture_c   ← c
【后置】fixture_a   ← a
【后置】fixture_b   ← b
pytest 按你在参数列表中写的顺序 fixture_b, fixture_a, fixture_c，执行

有依赖关系的fixture
@pytest.fixture
def db():
    print("【前置】db")
    yield "DB"
    print("【后置】db")

@pytest.fixture
def user(db):  # ← 依赖 db
    print("【前置】user")
    yield "User"
    print("【后置】user")

@pytest.fixture
def cache():
    print("【前置】cache")
    yield "Cache"
    print("【后置】cache")

@pytest.fixture
def service(user, cache):  # ← 依赖 user 和 cache
    print("【前置】service")
    yield "Service"
    print("【后置】service")

def test_service(service):
    print("测试 service")
    assert service
    
    
执行顺序：
【前置】db
【前置】user
【前置】cache
【前置】service
PASSED                                         [100%]测试 service
【后置】service
【后置】cache
【后置】user
【后置】db

db 必须在 user 前（依赖关系）
→ 先执行依赖的，完成后按书写顺序执行
→ 后置完全反向！

fixture 按依赖顺序实例化，对于同级 fixture，按书写顺序依次执行。
也就是说对于最终的fixture，传参接收的其它fixture先按书写顺序，依赖关系，作用域执行
针对单个时，又先运行其自己依赖的fixture
具体fixture依赖的运行关系见上述例子


conftest.py的作用域？conftest.py是什么？
作用域：
文件位置决定可见范围
位置                  范围
tests/conftest.py	tests/ 及其所有子目录下的测试文件
tests/api/conftest.py	tests/api/ 及其子目录下的测试文件
项目根目录 /conftest.py	整个项目所有测试文件

所以如果你进入一个文件夹下的某个模块中触发case，根目录下的conftest.py  和 当前文件夹目录下的conftest.py
会一起生效的，先执行父级的，再执行子级的（若作用域范围相同）

子目录的 conftest.py 会覆盖父目录的同名 fixture！

而fixture的作用域由scope参数决定，conftest.py只是定义位置，既说明这个
里边的fixture的影响哪些目录下的文件，而真正决定是在函数还是类还是整个会话的由
里边具体fixture的scope参数决定

你可以有全局的fixture，定义在根目录下的conftest.py
你也可以为每个目录设置专用的fixture
根目录下的设置为全局的前置后置
对应目录下的设置为该文件夹下的前置后置
且如果和父级同名，那么以该文件夹下的conftest.py写的fixture的逻辑为准

对我们来说的最佳实践就是
1.按模块或者功能来组织conftest.py，比如说你有全局共享的fixture 也有每个如api测试专用的fixture，有UI测试专用的fixture
2.避免在根目录放置太多的fixture，根目录只放置真正的全局的fixture，比如说，配置，日志，数据库连接等，其它按功能拆分，避免污染命名空间
3.可以利用覆盖机制做好环境隔离，覆盖机制并不是指父级同名的不执行，而是执行了，但是被覆盖掉了


什么是conftest.py文件，为什么会有这个文件？目的或者作用是什么？
作用：conftest.py 是 pytest 的“本地插件”文件，用于在特定目录下自动共享 fixture、hook 函数、自定义配置等，无需手动导入
它的目的是“让测试代码更干净、更模块化、更可复用”

首先不是必现写的文件，也不是配置文件，是pytest可以自动发现并加载的本地插件
核心特性就是放在某个目录下，针对这个目录及其所有子目录下的测试文件，都可以自动
的使用这个conftest.py里面定义的fixture和hook，不需要import

如果没有conftest.py，那你所有的测试文件都需要手动导入fixture
如果有，那你就不需要import，而可以直接将fixture传入测试用例

conftest.py的核心作用：
1.共享fixture
自动注入，不需要import
2.按目录隔离测试
你可以设置全局的fixture，还可以根据文件夹如api ui 设置每个子目录下的专用fixture
3.还可以结合pytest hook函数 自动生效，无需注册
这个后续学习到了再了解，目前没有用过
4.注册自定义命令行参数-这个后续学习到了再了解，目前没有用过
5.插件式扩展pytest的功能
比如注册自定义的marker，修改测试收集行为，添加自定义报告等-这个后续学习到了再了解，目前没有用过






params-参数化-支持list tuple  字典列表[{},{},{}]、字典元组({},{},{})

ids-当使用params参数化时，给每一个值设置一个变量名
你没有这个的时候，如果你参数化执行
每个参数的用例名后面都是一串字符，用了这个后，可以美化名称

name-给表示的是被@pytest.fixture标记的方法取一个别名
比如说例子中你给函数传递的是my_fixture
那你现在起了别的名字比如说aaa，那你后续传递aaa即可
一旦给它起了别名后，此时再用原来的名称是不行的，不能再用原来的名称


场景-1，你的类中2个方法或者更多的方法，此时你想让每个都有前置执行和后置执行
你使用autouse功能就可以，改为True,就会自动使用，不用你去调用这个



我们在项目中使用前置或者后置的夹具时肯定不希望让其和用例混在一起，希望框架能够更加的模块化
主要通过conftest.py+@pytest.fixture()结合使用，实现全局的前后置操作
比如项目的全局登录

1.conftest.py文件是单独存放夹具的配置文件，名称不能够更改
2.用处，可以在不同的py文件中使用同一个fixture函数
3.原则上这个文件需要和我们运行的用例在同一个层级，并且不需要任何导入的操作
但是实际上不是同一层，也是可以用的，因为conftest.py的作用域是同级目录及其相关子级下
所以我们比如说有2个模块的case
我们根目录下有这个conftest.py文件
2个模块下也各自有自己的conftest.py，模块内用自己模块的，执行没有问题
然后我们也可以在模块的使用用全局的夹具，你只需要写这个全局的夹具名称就可以，注意self后夹具的顺序
按ascii码，从小到大执行，如果有fixture依赖，注意相关逻辑的运行顺序
如果是要传入的这种，那你默认是没有开autouse的

在pytest中，conftest.py 文件的作用范围是其所在的目录及其子目录。
因此，A 模块的 conftest.py 文件中的夹具（fixture）默认情况下只能被 A 模块及其子模块中的测试文件使用
而不能直接被 B 模块下的文件使用。

对于pytest前后置操作的夹具，我们有以下总结：
1.我们使用setup/teardown  setup_class/teardown_class来实现每个用例或者每个测试类的前后置
目前已经不适用，通常使用下面2种方案来完成
2.使用@pytest.fixture(),在模块内实现夹具，但是一般这种不好复用，我们一般还是建议通过第三种方式来实践
3.使用conftest.py+@pytest.fixture()实现全局的夹具配置

对于夹具fixture，scope支持的参数中，如果除function(默认作用域为function)作用域外，因为如果是function
我不开自动，还是可以显示调用的，直接传进去，那么针对如果作用域是class，我不开autouse，我该怎么显示调用呢？
其他支持的参数为，如class/module/session
如果我不设置autouse = True，那么这个夹具该如何触发呢？或者我该如何显示的调用这些夹具？
第一种方式是使用 @pytest.mark.usefixtures('夹具名称')
比如说你的夹具scope是class时，你只需要在这个类名称的上面使用即可，举例如下-
import pytest

@pytest.fixture(scope='class')
def my_fixture():
    print("\nSetup in my_fixture")
    yield
    print("\nTeardown in my_fixture")

@pytest.mark.usefixtures("my_fixture")
class TestMyClass:
    def test_one(self):
        print("Running test_one")
        assert True

    def test_two(self):
        print("Running test_two")
        assert True
        
在这个例子中，my_fixture 将被应用到 TestMyClass 类中的所有测试方法 (test_one 和 test_two)。
    
第二种是给测试用例传参，像作用域为函数时那样使用，那么在触发这个用例时就会执行相关的夹具
如果你仅传递1个，前提是你的这个用例必须第一个执行，因为这样才能触发
如果你有5个。你仅仅给最后一个传这个，那样会等到第5个执行时才去，执行夹具的操作
如果你给所有的测试用例都传，那么也只会生效1次，但是会保证你的预期


在pytest中，fixture 的返回值只有显式请求该 fixture 的测试用例才能直接访问
具体来说，如果一个 fixture 被定义并返回某个值
只有那些通过参数传递的方式显式请求该 fixture 的测试函数才能接收到并使用这个返回值


Fixture 返回值的访问方式
显式请求 Fixture
当你在测试函数中将 fixture 作为参数传递时，pytest 会自动调用该 fixture，并将其返回值传递给测试函数。
import pytest

@pytest.fixture
def my_fixture():
    return "fixture data"

def test_with_fixture(my_fixture):
    print(f"Fixture returned: {my_fixture}")
    assert my_fixture == "fixture data"

在这个例子中，test_with_fixture 函数通过将 my_fixture 作为参数传递，显式请求了该 fixture
因此，它能够接收并使用 my_fixture 的返回值 "fixture data"

使用自动方法-自动使用 Fixture (autouse=True)
即使你设置 autouse=True，使得 fixture 在每个测试函数或类中自动应用，fixture 的返回值也不会自动传递给这些测试函数
autouse=True 主要用于执行一些全局的设置或清理操作，但不会影响测试函数的参数列表
所以，如果你要使用fixture的值，你就要显示的传参才可以




‌夹具返回值共享‌
如果你希望多个测试函数共享同一个 fixture 的数据，可以通过以下几种方式实现：
1.使用类级别的fixture-你可以将 fixture 设置为 scope='class'
并在测试类中显式请求该 fixture。这样，所有测试方法都可以共享同一个 fixture 实例及其返回值。
import pytest

@pytest.fixture(scope='class')
def class_fixture():
    print("\nSetup in class_fixture")
    yield "class_fixture_data"
    print("\nTeardown in class_fixture")

class TestClass:
    def test_one(self, class_fixture):
        print(f"Running test_one with fixture data: {class_fixture}")
        assert class_fixture == "class_fixture_data"

    def test_two(self, class_fixture):
        print(f"Running test_two with fixture data: {class_fixture}")
        assert class_fixture == "class_fixture_data"
        
在这个例子中，TestClass 类中的所有测试方法都可以访问 class_fixture 的返回值 "class_fixture_data"。       
        
2.使用模块级别的fixture-类似地，你可以将 fixture 设置为 scope='module'，以便在整个模块范围内共享 fixture 实例及其返回值。
import pytest

@pytest.fixture(scope='module')
def module_fixture():
    print("\nSetup in module_fixture")
    yield "module_fixture_data"
    print("\nTeardown in module_fixture")

def test_module_one(module_fixture):
    print(f"Running test_module_one with fixture data: {module_fixture}")
    assert module_fixture == "module_fixture_data"

def test_module_two(module_fixture):
    print(f"Running test_module_two with fixture data: {module_fixture}")
    assert module_fixture == "module_fixture_data"
    
在这个例子中，test_module_one 和 test_module_two 都可以访问 module_fixture 的返回值 "module_fixture_data"。


如果夹具需要返回值（如数据库连接），‌仅显式调用夹具的用例能直接访问返回值‌
其他用例需通过其他方式（如类属性）间接获取‌
所以综上所述，如果要用fixture返回的值，无论你是否打开 autouse，你都必须要手动
的显示将其传递给测试用例才可以使用


测试用例依赖多个fixture，且fixture还依赖其他fixture，那对于这个测试用例，fixture的执行顺序是什么？
测试用例依赖多个 fixture 时，执行顺序不是按“你写的参数顺序”
而是 pytest 根据“依赖拓扑关系 + 同级字母序”自动计算的
如果 fixture 还依赖其他 fixture，pytest 会递归解析依赖树
确保所有前置按依赖顺序执行，所有后置按反向顺序执行！
共同依赖，仅执行1次，比如说 B C 均依赖A，你传B C 那么A也仅执行1次
同一个fixture 在 同一作用域内仅执行1次
后置的执行顺序相反


作用域是否会影响执行时机？作用域和依赖关系，到底先看谁？ASCII码的排序和作用域先看谁

依赖关系，决定谁必须在谁之前执行，作用域决定何时初始化/清理，两者协同工作，但是依赖关系是硬约束
作用域是软调度

1. 收集所有需要的 fixture → 构建“依赖图”
2. 解析依赖关系 → 确保“被依赖的必须先初始化”
3. 根据 scope 调整“初始化时机”：
   - session: 在所有测试之前初始化
   - module: 在模块内第一个测试之前初始化
   - class: 在类内第一个测试之前初始化
   - function: 在每个测试之前初始化
4. 执行前置（setup）→ 按“依赖+scope”综合顺序
5. 执行测试
6. 执行后置（teardown）→ 按“完全反向”顺序

作用域冲突时的处理：直接报错，session 级别的 fixture 不能依赖 function 级别的 fixture！
规则：被依赖的 fixture 的作用域，必须 ≥ 依赖者的作用域

ascii  依赖关系 作用域，在测试用例依赖多个fixture时的优先级
场景：先依赖A，但是作用域是函数，再依赖B，作用域是session，难道A先执行吗？

A 不会先执行！pytest 会先执行 B（session），再执行 A（function）
因为 session 作用域的 fixture 必须在所有 function 作用域 fixture 之前初始化
无论参数书写顺序或“表面依赖”关系如何！作用域决定“生命周期层级”
是比参数顺序更底层的调度规则！

收集所有直接依赖：fixture_a, fixture_b
检查它们的作用域：
fixture_a → function
fixture_b → session
按作用域层级排序（session > module > class > function）
session 级别必须最早初始化、最晚清理
function 级别在每次测试前初始化、测试后清理
然后才考虑同作用域内的字母序或依赖关系


1️⃣ 作用域层级（session > module > class > function）→ 决定“何时生、何时死”
2️⃣ 依赖关系（A 依赖 B → B 必须在 A 前初始化）→ 决定“谁能用谁”
3️⃣ 同作用域内字母序 → 决定“同级谁先初始化”
4️⃣ 参数书写顺序 → ❗完全忽略！

直接就是session的先执行，依赖的有session的后执行
同级别的按写的先后顺序执行
被依赖的在依赖的之前先被执行
如果你确实需要控制同作用域、无依赖的 fixture 初始化顺序，你应该
1.显示的建立依赖关系
2.合并成一个fixture，不要依赖参数书写的顺序来控制关键的初始化顺序


实际测试结果
1 作用域为session  无任何依赖
2 作用域 function 依赖 3 session
4 作用域 function 依赖 5 session

测试用例依赖(2, 4, 1)
执行顺序 1 一定会先执行，接下来执行  2的依赖3 4的依赖5  2本身  4本身

测试用例依赖(4, 2, 1)
执行顺序 1 先执行，接下来执行4的依赖5  2的依赖3  再执行4本身  2 本身






pytest的断言
我们一般使用assert 断言，响应结果，看预期和实际是否匹配
对比如unittest的assertEqual assertTrue 等，直接使用python原生的assert
支持复杂表达式  如 in is not and or == < > 
支持自定义错误信息
支持异常断言，警告断言等专用工具

基本比较：
def test_basic_assert():
    a = 3
    b = 5
    assert a + 2 == b  # ✅ 通过

    c = 10
    assert a * b == c  # ❌ 失败，pytest 会打印：
    # >       assert a * b == c
    # E       assert (3 * 5) == 10
    # E         (15 == 10)
    
    
成员判断
def test_in_assert():
    fruits = ["apple", "banana", "cherry"]
    assert "apple" in fruits        # ✅
    assert "orange" in fruits       # ❌
    # E       assert 'orange' in ['apple', 'banana', 'cherry']
    # E         'orange' is not in the list
    
布尔值判断
def test_bool_assert():
    flag = True
    assert flag                     # ✅
    assert not flag                 # ❌
    # E       assert not True
    # E        +  where True = flag
    

自定义错误信息
def test_custom_message():
    x = 5
    y = 10
    assert x > y, f"期望 x({x}) 大于 y({y})，但实际不是！"
    # ❌ 输出：
    # E       AssertionError: 期望 x(5) 大于 y(10)，但实际不是！
    # E       assert 5 > 10
    
    
集合 字典 列表断言
能智能的对比容器类型，比如说字典是否相等，列表是否相等

建议
多个断言分开写，方便定位问题
    
    
    



pytest结合allure-pytest插件生成allure测试报告
之前我们使用的是pytest-html报告
现在我们使用allure报告，先下载安装解压，然后将bin目录添加至环境变量path中
验证-allure --version，如果pycharm验证时不能用，重启即可

首先生成json格式的临时报告
直接在pytest.ini  addopts中添加--alluredir ./temp

生成allure报告,这个可以添加在pytest.main()下面
os.system('allure generate ./temp -o ./report --clean')
./temp 临时的json文件格式的报告
-o 输出 
./report 生成allure报告路径
--clean 清空原来这个路径下的报告
如果显示你的 --alluredir ./temp 不可用
那你一定要记得安装 allure--pytest插件才可以


针对上述仅仅是pytest框架的基础知识，我们还有如下提升，针对框架的易用性或者模块化做提升
1.断言的封装
2.allure报告定制
3.关键字驱动-结合数据驱动实现接口自动化
4.python的反射机制
正向---在python中我们要使用类中的方法，一般是要先初始化对象，再调方法
反射---通过对象得到类对象，然后通过类对象调用方法
5.Jenkins的持续集成+allure报告集成，根据自动化报告的错误率来发送电子邮件
6.配置文件使用yaml,通过读取yaml文件来读取相关项目的配置
7.接口自动化pytest框架中，读取关联文件，比如说token
8.日志logging模块的集成，封装
9.flask框架+mock server，来模拟接口测试服务端



requests库是用来发送http请求以及接收http响应的python第三方库，https也是可以的
这个模块主要用于接口的自动化测试中实现对接口的请求
安装：使用pip install requests
常用方法：
1.requests.get()  
url用来写接口地址、params传参
一般我们看到的get请求的url，？后面都是params的参数
作为这个url的时候要去掉，参数拿出来要写到params中去
这个请求是有返回的，所以可以用变量来接，是你接口的response，你可以打印它
比如aa = requests.get(url='', params='')
print(aa.json())

2.requests.post() 
url用来写接口地址、参数传递一般是data传参或json传参
分析请求头，可以判断是用data传参还是json传参
依赖请求头的content—type来区分
请求头中的content-type作用
这是规定了服务器要求传入的报文的内容类型或者称为数据格式
请求的4个主要要素：
请求方式 
请求url  
请求头  
请求正文：入参
如果除了请求头，你其他正常写了，你的接口还是不通，那就要考虑看请求头是否有固定的参数

通过content—type区分在使用post请求时，是使用data传参还是json传参
请求头中的content-type，规定了服务器要求传入的报文的格式类型（数据格式）
比如：一般我们使用接口工具时，在postman中传参，针对post请求，是在body中传参
其有以下数据格式来传参
form-data  有文件上传
x-www-form 表单提交
binary 二进制方式
raw 里有很多格式text xml javascript html
我们常用的是json，请求头中就是application/json

1.data参数用于发送表单数据
content-type是application/x-www-form或者multipart/form-data
传入字典时，requests 会自动的将其编码为 application/x-www-form-urlencoded 格式（类似 key1=value1&key2=value2）。
传入字符串时，requests 会原样发送，你自己需要设置正确的content-type
传入文件时，会自动使用 multipart/form-data
举例：
import requests

url = "https://httpbin.org/post"

# 情况1: 发送普通表单数据 (字典)
response = requests.post(url, data={"username": "alice", "password": "secret123"})
# 请求头自动包含: Content-Type: application/x-www-form-urlencoded
# 请求体是: username=alice&password=secret123

# 情况2: 发送原始字符串 (需手动设置 Content-Type)
xml_data = "<user><name>Alice</name></user>"
response = requests.post(url, data=xml_data, headers={"Content-Type": "application/xml"})
# 请求体会原样发送 xml_data 字符串

# 情况3: 上传文件 (自动 multipart/form-data)
with open("example.txt", "rb") as f:
    response = requests.post(url, files={"file": f})
# 或混合表单和文件
response = requests.post(url, data={"description": "My file"}, files={"file": f})

使用data传参
1.若要求入参是dict类型，既入参是发送普通的表单数据，正常的python字典，key/value，无嵌套
默认的content-type要求是x-www-form，requests会自动将其编码为表单数据
举例-
import requests

url = "https://example.com/api"
payload = {
    'key1': 'value1',
    'key2': 'value2'
}

response = requests.post(url, data=payload)
print(response.text)
在这个例子中，requests 会将 payload 编码为 application/x-www-form-urlencoded 格式
并发送如下形式的数据：key1=value1&key2=value2

2.如果你需要上传文件以及其他部分的数据，可以使用 files 参数与 data 参数结合：
files = {'file': open('report.xls', 'rb')}
response = requests.post(url, data=payload, files=files)


3.使用data传参，要求报文是字符串类型，默认的content-type要求为text/plain 
问：上面这个2说的对吗？
答：实际上，当你使用 requests 库的 data 传参时，参数为字符串时
需要你自己手动明确设置content-type
默认情况下并不会自动设置 Content-Type 为 text/plain，而是用户自己显示指定
只有当你传递的比如说是普通的表单数据时，既就是python无嵌套的字典作为入参时
请求头自动包含: Content-Type: application/x-www-form-urlencoded
requests 库会根据你传递的数据类型，来决定如何编码数据并设置相应的 Content-Type 头。

使用data传参的默认行为：
字典形式的 data，无嵌套字典：
如果你传递的是一个字典，requests 会将其编码为 application/x-www-form-urlencoded 格式。
默认的 Content-Type 会是 application/x-www-form-urlencoded。

字符串形式的 data：
如果你传递的是一个字符串，requests 不会对其进行任何编码，直接发送该字符串。
默认情况下，Content-Type 不会被设置（即没有 Content-Type 头），你需要手动设置content-type。

文件上传（multipart/form-data）：
如果你的请求涉及文件上传，那你可以使用 files 参数进行文件上传，若有其他数据，可以使用data传参
requests 会将 data 和 files 组合成 multipart/form-data 格式
并设置相应的 Content-Type：multipart/form-data

如果你需要发送纯文本数据并且要求 Content-Type 为 text/plain
你需要手动设置 Content-Type 头。
import requests

url = "https://example.com/api"
payload = "This is a plain text payload"

headers = {
    'Content-Type': 'text/plain'
}

response = requests.post(url, data=payload, headers=headers)
print(response.text)
在这个例子中，payload 是一个字符串，我们通过 headers 参数显式设置了 Content-Type 为 text/plain。

具体示例对比
为了更清楚地说明不同情况下的行为，以下是几种不同的场景和对应的 Content-Type 设置：
字典形式的 data 参数
import requests

url = "https://example.com/api"
payload = {
    'key1': 'value1',
    'key2': 'value2'
}

response = requests.post(url, data=payload)
print(response.request.headers['Content-Type'])  # 输出：application/x-www-form-urlencoded
在这种情况下，Content-Type 自动被设置为 application/x-www-form-urlencoded。




3.使用json传参，json参数用于发送json数据（既application/json编码的数据）
当你传入一个字典或者列表作为入参时
requests会自动调用json.dumps()自动将其序列化为json格式的字符串
并自动的设置正确的请求头-content-type为，application/json
支持包含嵌套对象，数组等复杂的结构
举例：
import requests

url = "https://jsonplaceholder.typicode.com/posts"

# 发送 JSON 数据
payload = {
    "title": "foo",
    "body": "bar",
    "userId": 1
    # 可以包含嵌套对象、数组等复杂结构
    # "tags": ["python", "requests"],
    # "metadata": {"created_by": "user123"}
}

response = requests.post(url, json=payload)
# ✅ requests 自动做了两件事：
# 1. 将 payload 序列化为 JSON 字符串: '{"title": "foo", "body": "bar", "userId": 1}'
# 2. 设置请求头: Content-Type: application/json

print(response.request.headers['Content-Type']) # 输出: application/json
print(response.request.body.decode('utf-8'))    # 输出: {"title": "foo", "body": "bar", "userId": 1}

举例-
import requests

url = "https://example.com/api"
payload = {
    'key1': 'value1',
    'key2': 'value2'
}

response = requests.post(url, json=payload)
print(response.text)
在这个例子中，requests 会将 payload 序列化为 JSON 格式，并发送如下形式的数据：
{
    "key1": "value1",
    "key2": "value2"
}
并且，请求头中会包含：Content-Type: application/json


一般来说报文可以是dict也可以是str
只要你默认的content-type要求是application/json，你就可以使用json来传参
问：那你怎么知道服务器要求什么格式的参数呢？
答：第一种接口文档说明，第二种抓包工具抓包，第三周，尝试验证，第四种
根据经验，比如说嵌套对象或者数组数据，则大概率是json，如果是简单的用户名密码
等了，搜索表单，可能是data，如果需要上传图或者文件，必须使用file，则这个属于data的范畴

所以也可以这样说
如果参数是纯键值对的字典，可以通过data传参，也可以json，取决你这个接口的请求头中content-type的要求
data可以传纯键值对的dict，纯{key：value}-无嵌套，也可以传str格式
如果content-type要求是application/json，而你一定要使用data来传，那你就需要使用json.dumps()
把它转化为json字符串就可以使用 data 传参了，且需要你显示的设置headers
然后可以手动设置content-type为application/json
import json

requests.post(url, data=json.dumps(payload), headers={"Content-Type": "application/json"})

如果入参本就已经是一个json格式的字符串，那建议你使用data传参，且显示声明header中的content-type
如果你依然用json传参，那么json.dumps(json) 可能转换出来的数据并不是你期望的入参

json可以传任何形式的字典（包括嵌套）
上面说的嵌套是这种 {a:{a:b,c:d}}
无嵌套的就是{a:b,c:d,e:f}这种

使用下面2种方法需要先
import json
json.loads()把json字符串转换为dict格式
json.dumps() 把dict格式转化成json字符串

JSON 字符串 是一种文本格式，遵循特定的语法规则，主要用于数据传输和存储。
Python 字典 是 Python 内置的数据结构，可以直接操作和修改。
json.dumps() 用于将 Python 对象转换为 JSON 字符串。
json.loads() 用于将 JSON 字符串解析为 Python 对象。

那我们还有一种思路本来必须要json传参的，我们可以把参数
使用json.dumps(data)转换为字符串格式后，再使用data传参,并且显式的声明header中的content-type为
application/json即可
这种就是比如说你的入参是一个嵌套字典，你还要使用data传参的情况
注意需要手动设置请求头content-type为application/json

requests.put()
requests.delete()

requests.request()可以发送所有类型的请求，get/post/put/delete，这个方法是比较万能的，它接收如下参数，method  url 和其他相关参数


requests 模块，发出相应的接口请求后，返回的response对象相关用法详解
res.json() 将响应体解析为python的字典或列表格式的数据，要求响应是json格式
res.text()  获得返回的响应体，字符串类型
res.content 获得返回的bytes字节类型的数据，用于下载图片或文件
res.status_code 获得返回的http的状态码
res.reason() 返回状态信息
res.cookies() 服务器返回cookie信息
res.headers()   返回响应头，字典类型
res.request.xxx 后面可以接很多方法，返回请求的数据，如请求头，请求参数
.ok 布尔值，判断状态码是否小于400 


问：怎么理解json
答：可以这样说，是字典类型的字符串格式

问：一般如何保持上下文会话的一致性？
普通方法，接口依赖token或者cookie，你该如何让下一个接口使用这个cookie或者token？
调登录接口后，从返回值使用 response.cookies 提取到cookie，注意使用这种方式提取到的
才可以在requests的接口请求参数，cookies中使用，
要么你就要显示的将cookie从浏览器开发者模式的请求头中取出来后，写进header中，然后
使用requests请求时，使用headers参数将header传进去
例子如下：
headers = {
    "Cookie": cookies
}
# response_2 = requests.get(url=url_2, cookies=extracted_cookies) # 
response_2 = requests.get(url=url_2, headers=headers)
print(response_2.json())

上述的方法仅仅用于日常简单的通过代码请求接口时的练习，如果你要用于自动化测试
这样肯定是可以，但是不够方便，首先代码层面不简洁，其次你可以用更加优美的方法
使用Session，Session 对象的 .cookies 属性是一个 RequestsCookieJar 对象，功能强大
使用requests.Session() 适合登录后操作，多步骤流程，自动化测试，自动管理cookie，保持当前会话的唯一性，代码简洁，高效复用连接
举例
import requests

session = requests.Session()

# 1. 手动设置 Cookie 到 Session
session.cookies.set('my_cookie', 'my_value', domain='httpbin.org', path='/')
# 等价于: session.cookies['my_cookie'] = 'my_value' (但推荐用 set)

# 2. 查看当前 Session 的所有 Cookie
for cookie in session.cookies:
    print(f"{cookie.name} = {cookie.value} (Domain: {cookie.domain}, Path: {cookie.path})")

# 3. 删除某个 Cookie
session.cookies.pop('my_cookie', None)
# 或 session.cookies.clear() # 清空所有

# 4. 从文件加载/保存 Cookie (模拟浏览器持久化)
import pickle

# 保存 Cookie 到文件
with open('cookies.pkl', 'wb') as f:
    pickle.dump(session.cookies, f)

# 从文件加载 Cookie
with open('cookies.pkl', 'rb') as f:
    session.cookies.update(pickle.load(f))





一般不直接的使用requests.get()等这些方法，而是使用公共的requests.request()方法来完成
为什么这样使用？目的就是为了封装统一请求的公共方法，其实我们使用的requests.get()
最终底层是用的requests.request()，而此方法,最终使用的是Session.request()方法
针对Session.request()方法，接收如下参数，接收很多，我们挑重要的说
method 请求方式
url 请求路径
params=None get方式传参
data = None post方式传参
json = None post方式传参
headers=None 请求头
cookies=None 请求cookies
files = None 文件上传

文件上传，首先使用file参数，其次是一定要把这个资源使用open打开，转换为二进制来完成
举例-
如果你需要上传文件以及部分其它的数据，可以使用 files 参数与 data 参数结合：
files = {'file': open('report.xls', 'rb')}
response = requests.post(url, data=payload, files=files)


五-请求必须带请求头的接口，以及需要cookie鉴权和session鉴权的接口
如果有cookie鉴权，有以下2种解决方式
1.使用cookie 关联
如上述的2种方式，要么.cookies取出后，下一个请求使用cookies传参,要么直接
开发者模式取出cookie，然后header显示的写出来，完了通过headers传参即可

2.使用session关联,我们没有传递cookie也可以访问成功
一般情况下我们使用会话的方式，来完成请求，cookie会被自动添加管理，复用，从而保证上下文会话的一致性
如果使用cookie关联，接口数量多的话，会做很多重复的工作，代码冗余

requests.Session()能够实现接口关联的核心原因在于它能够自动管理 Cookie 和其他请求上下文信息（如认证、头部信息等）
这种特性使得多个请求之间可以共享会话状态，从而模拟用户登录后的操作或实现接口之间的关联。

requests.Session() 创建一个会话对象，该对象会在多个请求之间保持会话状态。它的主要功能包括：
自动管理 Cookie：当服务器返回响应时，会话对象会自动存储响应中的 Cookie，并在后续请求中自动附带这些 Cookie。
持久连接：通过复用底层 TCP 连接，减少网络开销，提高性能。
共享请求配置：可以在会话对象中设置全局的请求参数（如 headers、auth 等），这些参数会被所有基于该会话发出的请求继承。


为什么 Session 可以实现接口关联？
(1) 自动处理 Cookie
许多接口需要通过 Cookie 来维持用户的会话状态
例如，在登录接口成功后，服务器会返回一个包含会话标识（如 session_id）的 Cookie。
如果后续接口需要依赖这个会话标识来验证用户身份，则必须将该 Cookie 带到下一个请求中
使用 requests.Session() 时，会话对象会自动保存和携带 Cookie，无需手动管理。
import requests

# 创建会话对象
session = requests.Session()

# 第一次请求：登录接口
login_url = "https://example.com/login"
login_data = {"username": "user", "password": "pass"}
response = session.post(login_url, data=login_data)

# 第二次请求：访问需要登录状态的接口
protected_url = "https://example.com/protected"
response = session.get(protected_url)

# 此时，会话对象会自动携带登录时获得的 Cookie
print(response.text)

如果没有使用 Session，则需要手动提取并传递 Cookie，代码会变得复杂且容易出错。

(2) 共享请求配置
在接口测试中，某些请求可能需要相同的头部信息（如 Authorization 或 User-Agent）
通过 Session，可以一次性设置这些参数并让所有基于该会话的请求继承这些配置
session = requests.Session()
session.headers.update({"Authorization": "Bearer your_token_here"})
# 所有基于此会话的请求都会自动带上 Authorization 头部
response = session.get("https://example.com/api/resource")

(3) 模拟真实的用户行为
在实际应用中，用户的行为通常涉及多个接口的交互（如登录 -> 获取数据 -> 提交数据）
使用 Session 可以很好地模拟这一过程
因为会话对象能够持续维护状态，确保每个请求都基于前一个请求的结果

总结：
requests.Session() 的核心优势是自动管理 Cookie 和共享请求配置，从而实现接口之间的状态关联。
在接口自动化测试中，使用 Session 可以显著简化代码逻辑，提升开发效率。
如果你的接口测试场景涉及多个需要共享状态的请求，强烈建议使用 requests.Session() 来管理会话。


目前接口测试的主流框架或者技术-python+requests+pytest+allure+Jenkins
上述我们学习了requests库，接下来我们继续学习pytest-这是我们的一个用例管理框架





pytest中默认的测试用例识别规则：
模块名必须以test_ 开头  或者_test结尾
测试类必须以Test开头，并且不能有__init__方法
测试用例必须以test_开头

我们使用pytest框架时，需要很多的插件,可能这段代码在你的电脑本地能跑，那如何保证
在服务器上，在别人的电脑上也可运行呢？再比如你的A项目依赖的插件版本是1.0，B项目依赖同样的插件
但是版本是2.0，你该怎么办呢？我们使用虚拟环境配置即可，既创建项目时，就创建一个虚拟环境
基于虚拟环境
我们可以给项目的根目录下，新建一个 requirements.txt文档，来说明我们需要的插件以及版本
pytest
pytest-html
pytest-xdist
pytest-ordering
pytest-rerunfailures
allure-pytest
这些被安装的插件仅仅针对这个虚拟环境有用


pytest的运行方式详解
1.命令行模式
终端进入到项目内，运行pytest
-v  输出更加详细的信息
-s  输出调试信息，打印相关的print信息
-vs 组合使用
-n  多线程运行测试用例
--reruns 失败后运行几次   3用例 其中1失败 参数重复执行2   那共执行5次  结果1失败 2通过
--html ./reports/report.html 生成html的测试报告   前提当前目录下要有report文件夹
-m smoke  执行标记的用例smoke
2.主函数模式
根目录下新建 all.py
import pytest

if __name__ =="__main__":
    pytest.main()
执行即可，可以直接给main方法中增加相关参数，也可以结合pytest.ini一起使用

结合pytest.ini配置文件运行
[pytest]
addopts = -vs
testpaths = ./testcase
python_files = test_*.py
python_classes = Test*
python_functions = test_*
markers = 
        smoke:冒烟测试    #这个就是给用例打标记，结合@pytest.mark.smoke
        
        
三.pytest测试用例的前后置-叫夹具或者固件
1.我们一般既想在一个测试用例前和后执行的操作，普通方法可以使用
setup/teardown，在每个用例前后都要执行
setup_class/teardown_class 每个类之前之后执行
问：但是无法满足这样的场景，比如说我仅仅是几个case需要连接数据库，不是所有的用例，那我怎么做？
答：pytest提供了更加灵活的用例前后置，使用@pytest.fixture装饰器实现部分用例的前后置操作
支持参数：
1.scope 作用域  function class module  package session
2.pytest.fixture 中的 scope 参数确实可以设置为 session，表示该 fixture 的作用范围是整个测试会话（session）。这意味着在整个测试运行期间
fixture只会被初始化一次，并在所有需要它的测试用例中共享。
3.params 参数化fixture
4.autouse 是否自动开启，默认False 需要手动传入才可以
5.ids
6.names


接口的关联和封装
这样一个场景，A目录下有测试模块1.py，2.py，我现在1下有3个case，2下有2个case
但是2中因为要保持会话一致性，比如说可能1里边写的登录，完了后
2要作为1这个用于完成一些业务，比如说设置头像/个人说明等，
那就要用1中的这个测试类下的requests.session(),来确保对话的一致性
所以导入了这个类，但是导入的这个类下挂了3个case
此时A目录下执行pytest  发现1被执行3次，然后执行2的2个用例，本来预期的5，变成了执行8条

原因：2为了用1这个类（这个类下挂了3个case)中的一个方法
导入了1的这个类时，也就意味着你导入了这个类的所有，包括这个方法和其他3个case
所以间接的让这个2有了5个测试用例，所以结合pytes框架的特点，这些case会再次被执行
所以就导致出现了这个问题，相当于是pytest间接发现了，然后又执行了一次

上述这个情况--我们也可以称之为继承，看起来，继承会出现这个问题
我们该如何规避这个现象

1.如何封装？
接口自动化测试框架关于接口关联的封装
目的：既我们在当前的测试过程中要保持同一个会话，上述应该是碰到的第一个难点

策略-去掉全局变量，使用yaml文件替代保存
在我们上述例子中，项目下，1文件的全局变量 requests.session()
其他模块的业务要使用，这个会话，必须引用1模块，所以导致了这个问题

新建common层（python软件包）
新建yaml_util.py文件来进行对yaml文件的一些操作
读取

写入

清空

yaml文件本身，我们一般放在根目录下，比如extract.yaml
对于配置文件的读取写入消除，我们可以结合装饰器或者pytest框架中的fixture夹具
来完成对用例中一些公共配置的操作


接下来我们继续学习
1.接口自动化框架的封装-统一请求的封装，比如说你不同模块
2.allure报告生成
3.数据驱动
4.持续集成


接口自动化测试框架，关于接口关联的封装
1.策略-去掉全局变量(这里我们实现的是这个模块或者这个模块下这个测试类的全局变量）使用YAML文件替代
我们之前是采用在类中定义属性
在测试用例，也就是类的方法中，我们获取变量后赋值给属性，其他测试用例（方法）可以引用类的属性
但是如果此时我要有其他的模块要如何引用，通过读取yaml文件来使得不同模块的接口可以使用全局变量
我们想实现的比如说token，这种公共的，我们想让不同模块，不同类等都可以使用这个全局变量


2.我们之前拆分的时候，a模块的requests.session(),如果被引用到其他模块，你的目的是想让接口关联起来
既他们均发生在同一会话内，但是导入会使得这个被导入模块的case也会被导入进来，从而重复执行
因此我们之前采用的方式仅仅是解决了重复执行的问题，既A模块和B模块无任何联系，那A就不需要使用B的requests.session()

B可以新建requests.request()来发送请求，且这些接口不存在cookie关联，我们可以这样做，是因为这2个模块的接口没有关联
所以我们仅仅是解决了模块和模块之间的，我们之前的做法，如果是一个模块的接口关联可以使用类属性的方式来充当变量的传递
但是如果她们是有相互关联的模块，我们又有何措施呢？
答：
如果A B模块的接口存在cookie关联，既他们需要使用同一个session-使用以下策略
1.接口自动化框架封装-统一请求封装
a.目的-通过同一个session发送请求
b.方便我们统计用例，日志封装
我们继续在common层下新建请求模块request_util.py，开始封装



接口自动化框架封装之yaml数据驱动封装------pytest参数化实战***
1.第一种方式，我们驱动时，代码中的数据直接写死在case中，比如url，参数等
2.第二种方式，在我们的case层，一般我们是一个接口对应一个yaml文件。把我们这个接口需要的数据都写在yaml文件中
入参的数据格式就是纯键值对
3.断言

yaml文件格式要求
-代表你这个接口的第一组数据，这个data入参直接就是键值对，不要引号，不要逗号。举例如下

-
  name: 接口名称,如获取token接口
  request:
    method: 请求方式,如get
    url: url
    data:
      age: 18
      sex: boy
      country: china
  validate: 断言,比如None
-
  name: 接口名称,如获取token接口
  request:
    method: 请求方式,如get
    url: url
    data:
      age: ''
      sex: boy
      country: china
  validate: 断言,比如None

4.开始数据驱动使用
举例：
@pytest.mark.parametrize(参数名称，参数的值）
我们的值是从yaml文件中读取到的
所以yaml文件中新增一个读取测试用例数据的方法
def read_testcase(yaml_name):
    with open(os.getcwd() + '/testcase/' + yaml_name) as f:
        #先打开这个yaml文件，使用命令获取项目的根目录，然后+文件名称来拼接获取这个文件的路径即可
        #默认的mode 是只读模式
        value = yaml.load(stream=f, Loader=yaml.FullLoader)  #读取yaml文件的值，并返回
        return value
        
在你这个接口的上方使用装饰器
@pytest.mark.parametrize('args_name'，read_testcase('get_token.yaml'))
这个装饰器会把读到的数据，传给args_name，那你同样下方接口要使用的话，就把这个传给测试用例中
读取后的格式是一个字典，所以你这个接口中相关数据，都可以使用
args_name[key]来取值，具体还是结合你yaml文件中数据的格式
比如说你的url--args_name['request']['url']

使用数据驱动的目的就在于，一个接口或者一个测试用例，用例的数据有正向用例/反向用例
我们继续在这个当前接口的yaml文件中编写反例case，实现参数化，数据化驱动数据

如果我们要这样做，那对我们的要求就是，一定要在一个测试用例的逻辑层中做好判断
这样才能应对不同数据的情况下的case
比如说你正向的接口你的一些响应能获取到，并且你的这个值还要被后续接口使用
但是异常的用例，你可能要做一些相关的处理
比如说这个值在的时候，我再去执行一些我要的操作，我跑异常case的时候，我就不需要执行相关操作
然后再结合断言，来完成



那么其实我们也是需要对断言做封装的---待学习，教程没有说明


接口自动化allure报告的生成和定制
1.下载allure，并解压，配置bin目录环境变量至path路径下
2.终端 allure --version 看是否配置好，看不到重启pycharm
3.先生成临时的json报告，先在项目根目录下新建temp python软件包，用来保存临时的json报告
4.pytest.ini文件中 addopts新增   --alluredir ./temp --clean-alluredir
5.根据临时报告，生成html的allure报告
在run文件中，pytest.main()后新增执行系统命令
os.system('allure generate ./temp -o ./reports --clean')
加--clean就是因为要执行很多次，每次执行之前先清空，确保每次获取的都是最新的



通过上述的学习，这个简单的架构还存在一些问题，在企业项目中使用时--落地实施的问题
1.一个接口对应一个yaml文件，如果1个接口有30条用例，那么yaml文件会比较庞大---使用csv文件结合使用，需要学习
2.有些接口的参数是动态的，如何处理-比如说创建用户时你第一个入参的用户名，此时如果你要再创建，你就要修改你的接口，用起来不灵活
引申出来如何在yaml中处理动态参数---调用函数的方式，比如说加随机数
3.断言的封装
4.在yaml中文件上传怎么处理
5.搭建框架的投入和产出比---深度思考，如何能做到其他的测试不写代码，而使用你的框架实现别人业务的自动化
6.日志系统（模块）的搭建，明确的能展示出你接口用例的执行过程，比如接口名称，请求方式，请求路径，请求头，请求参数，文件上传，预期结果，实际结果
这些日志要能打印在控制台，也最好能分接口在你的测试报告的，这个测试用例的测试步骤中打印出来
7.最后的目的就是通过写yaml文件  csv文件就可以进行测试了


学习路线
python的入门到精通
接口自动化测试：接口测试理论-接口测试流程/策略/测试用例设计/测试执行/测试报告
             接口测试工具精通-postman/理解cookie/session/token/Charles抓包/jmeter
             pytest框架-框架简介/基础运用/运行方式/相关参数-失败用例再次执行/常用插件/运行参数/全局配置文件/用例分组执行，比如说
             执行冒烟测试/分组执行测试用例等/跳过测试用例，pytest参数化实现/pytest前后置
             requests功能模块
             pytest框架封装-requests二次封装/yaml测试用例二次封装/基础模块封装/日志模块集成/全局配置文件/report测试报告模块/pytest+allure
             md5加密/rsa解密，base_url基础路径封装/接口签名验证/MySQL数据库断言/Jenkins集成

web自动化测试
app自动化测试

一个接口自动化测试框架的基础构成：
common 存放公共的一些封装方法，包括日志封装/统一请求封装/yaml文件读取
data 存在csv数据文件夹
logs 存放日志文件
reports 存在allure报告文件夹
temp 存放临时的json报告
testcases 存放测试用例
all.py 总执行文件
config.yaml 总配置文件
conftest.py 总前后置方法
extract.yaml 存放接口关联时提取的变量
pytest.ini pytest的配置文件
debug_talk.py 热加载函数封装文件


接口自动化测试框架-数据用例的规则
必须有的4个一级关键字  name/base_url/request/validate
request，一级关键字下必须包含2个二级关键字method/url，至于是否有参数，若无则不填写，若有则
传参方式，在request一级关键字下，通过二级关键字传参
如果是get请求，通过params传参
如果是post请求，传json格式，通过json关键字传参；传表单格式，通过data传参
如果是传文件格式，通过files关键字传参


如果需要做接口关联，必须使用一级关键字extract 框架支持json提取
json提取方式
extract：
    access_token : access_token
    
正则提取
extract:
    access_token: '"access_token":"(.*?)"'
    
取值
${get_extract_data(access_token)}


5.热加载-当yaml文件使用动态参数时，那么可以在debug_talk.py文件中写方法调用，比如说你获取随机数的方法
6.框架支持2种断言 equals contains断言
validate:
-   equals: {status_code: 200}
-   contains: access_token
7.数据驱动，使用csv和一级关键字params实现，如
yaml写法
    parameters:
        name-appid-secret:  data/get_token.csv
        
csv写法：
    name-appid-secret
    获取统一接口鉴权码
    appid必填检查，，逗号隔开，后面可以写不同的正向/反向参数例子
    secret必填检查
8.日志监控，异常处理，及基础路径配置



requests模块的全局观
requests.get()
requests.post()
requests.delete()
requests.put()
requests.request()  核心方法
这些方法都会返回response对象
rep.text 返回字符串的数据---试用于接口返回文本格式数据
rep.content 返回字节格式的数据--接口返回的是文件
rep.json()   返回字典格式的数据
rep.status_code  状态码
rep.reason 返回状态信息
rep.cookies 返回cookie信息
rep.encoding 返回编码格式
rep.headers 返回响应头信息


get请求，通过params传参
post请求
data传参 报文为字典类型，比如说x-www-form时使用这个，表单的数据报文为str类型  默认为text/plain
json传参 默认是application/json，使用json传参

json.dumps() 序列化 把字典格式的数据转换为str格式
json.loads() 返序列化  把str格式转换为dict格式

框架中对于requests.session()的封装不能放在类中的方法，否则每次调用都会生成新的会话，导致前面的失效，和我们的预期不匹配
一般在实际的项目开发中，根目录都会说明这个项目的依赖、
如一个requirements.txt文档 说明插件和使用的版本
通过pip install -r requirements.txt 一次性安装项目需要的依赖，注意这是仅针对这个项目的虚拟环境安装

"""